﻿function $(selector) {
    /// <summary>Retrieves the class object for the specified contract ID.</summary>
    /// <param name="selector">The contract ID for which to retrieve a class object OR an interface ID to retrieve the XPCOM interface for.</param>
    /// <returns type="ComponentClass" />

    if (selector) {
        if (selector[0] === "@") {
            return new ComponentClass(selector);
        }
        else {
            return Components.interfaces[selector];
        }
    }

    throw new Error("The selector is required for the $() function.");
}

function ComponentClass(contractId) {
    /// <summary>Creates a new ComponentClass instance which facilitates access to common XPCOM functions.</summary>
    /// <param name="contractId" type="string">The contract ID for which to retrieve a class object.</param>

    this._componentClass = Components.classes[contractId];

    if (!this._componentClass) {
        var console = Components.classes["@mozilla.org/consoleservice;1"].getService(Components.interfaces.nsIConsoleService);
        console.logStringMessage("The contract ID '" + (contractId || "NULL") + "' did not yield a known component class.");
    }
}

ComponentClass.prototype = {
    instance: function(interfaceId) {
        /// <summary>Creates an instance of the component given an interface nsID for that component to conform to.</summary>
        /// <param name="interfaceId" type="string">The ID of the interface that is returned to represent a class instance.</param>
        /// <returns>An instance of a particular class given the specified interface ID.</returns>

        return this._componentClass.createInstance(Components.interfaces[interfaceId]);
    },

    service: function(interfaceId) {
        /// <summary>Gets a reference to a particular XPCOM service based on the nsID of an interface for that service to conform to.</summary>
        /// <param name="interfaceId" type="string">The ID of the interface that is returned to represent a service.</param>
        /// <returns>A reference to a particular XPCOM service.</returns>
        
        return this._componentClass.getService(Components.interfaces[interfaceId]);
    }
};

$.createCallback = function(callback, context) {
    var args = [];
    
    for (var i = 2; i < arguments.length; ++i) {
        args.push(arguments[i]);
    }
    
    return function() {
        try {
            $.verbose("Attempting callback.");
            var args2 = [];
            for (var i = 0; i < arguments.length; ++i) {
                args2.push(arguments[i]);
            }
            
            args2 = args2.concat(args);
            
            callback.apply(context, args2);
        }
        catch (e) {
            $.error(e);
        }
    };
};

// Ajax
$.cacheHandling = {
    useCache: 0,
    bypassCache: 1
};

$.ajax = $("@namfox.neoseeker.com/ajax;1").service("nfxIAjax");

$.get = function(url, data, success, error, cacheHandling, cookie) {
    /// <summary>Gets data at a specific URL.</summary>
    /// <param name="url" type="String">
    ///   The URL against which the request will be made.
    /// </param>
    /// <param name="data" type="Object">
    ///   A object of data to send.
    /// </param>
    /// <param name="success" type="Function" optional="true">
    ///   (Optional) The success callback.
    /// </param>
    /// <param name="error" type="Function" optional="true">
    ///   (Optional) The error callback.
    /// </param>
    /// <param name="cacheHandling" type="Number" optional="true">
    ///   (Optional) Specifies how to handle Firefox's cache when making a request.
    /// </param>
    /// <param name="cookie" type="String" optional="true">
    ///   (Optional) A string of cookie data to send with the request.
    /// </param>
    /// <returns type="nsIXMLHttpRequest" />
    
    return $.ajax.get(url, $.wrapObjectLiteral(data), success, error, cacheHandling || $.cacheHandling.useCache, cookie || "");
};

$.post = function(url, data, success, error, cacheHandling, cookie) {
    /// <summary>Gets data at a specific URL.</summary>
    /// <param name="url" type="String">
    ///   The URL against which the request will be made.
    /// </param>
    /// <param name="data" type="Object">
    ///   A object of data to send.
    /// </param>
    /// <param name="success" type="Function" optional="true">
    ///   (Optional) The success callback.
    /// </param>
    /// <param name="error" type="Function" optional="true">
    ///   (Optional) The error callback.
    /// </param>
    /// <param name="cacheHandling" type="Number" optional="true">
    ///   (Optional) Specifies how to handle Firefox's cache when making a request.
    /// </param>
    /// <param name="cookie" type="String" optional="true">
    ///   (Optional) A string of cookie data to send with the request.
    /// </param>
    /// <returns type="nsIXMLHttpRequest" />
    
    return $.ajax.post(url, $.wrapObjectLiteral(data), success, error, cacheHandling || $.cacheHandling.useCache, cookie || "");
};

// Custom messages
$.messages = {
    _manager: $("@namfox.neoseeker.com/custom-messages/manager;1").service("nfxICustomMessageManager"),
    
    backup: function(filePath) {
        /// <summary>Backs up the custom messages to a location specified by the user.</summary>
        /// <param name="filePath" optional="true">(Optional) The file path where custom messages will be saved.</param>
        
        this._manager.backup(filePath);
    },
    
    enumerate: function(callback) {
        /// <summary>Enumerates all of the NAMFox custom messages, except for the default message.</summary>
        /// <param name="callback" type="Function">The callback function, invoked for each message.</param>
        
        var keyCount = {};
        var keys = this._manager.messages.getKeys(keyCount);
        
        for (var i = 0, length = keyCount.value; i < length; ++i) {
            try {
                callback(this.getMessage(keys[i]).QueryInterface($("nfxINamedCustomMessage")));
            }
            catch (e) {
                $.error(e);
            }
        }
    },
    
    getDefaultMessage: function() {
        /// <summary>Gets the default custom message.</summary>
        /// <returns type="nfxICustomMessage" />
        
        return this._manager.defaultMessage;
    },
    
    getMessage: function(key) {
        /// <summary>Gets the custom message at a particular index.</summary>
        /// <param name="key" type="String">The key for the message to retrieve.</param>
        /// <returns type="nfxINamedCustomMessage"/>
        
        var message = this._manager.messages.getValue(key);
        
        try {
            return message.QueryInterface($("nfxINamedCustomMessage"));
        }
        catch (e) {
            $.error(e);
            return null;
        }
    },
    
    refresh: function() {
        /// <summary>Reloads custom messages from the default custom message file.</summary>
        
        this._manager.refresh();
    },
    
    restore: function(filePath) {
        /// <summary>Backs up the custom messages to a location specified by the user.</summary>
        /// <param name="filePath">The file path from which custom messages will be restored.</param>
        
        this._manager.restore(filePath);
    },
    
    save: function() {
        /// <summary>Saves the most recent custom message data to disk.</summary>
        
        this._manager.save();
    },
    
    size: function() {
        /// <summary>Gets the number of custom messages available.</summary>
        /// <returns type="Number" integer="true" />
    
        var keyCount = {};
        this._manager.messages.getKeys(keyCount);
        
        return keyCount.value;
    }
};

// Member Services
var memberService = $("@namfox.neoseeker.com/member-service;1").service("nfxIMemberService");
$.getCurrentUser = memberService.getCurrentUser;
$.refreshCurrentUser = memberService.refreshCurrentUser;

// Configuration
var configuration = $("@namfox.neoseeker.com/configuration;1").service("nfxIConfiguration");
$.getConfig = function() {
    /// <summary>Gets the NAMFox configuration object.</summary>
    /// <returns type="nfxIConfiguration" />
    
    return configuration;
};

var LinkBehavior = {
    OpenInNewTabWithMiddleClick: 1,
    AlwaysOpenInNewTab: 2
};

var StripOptions = {
    Color: 1,
    SizeOne: 2,
    Sub: 4,
    Sup: 8
};

// Data Cache
$.dataKeys = {
    quickReplyMessage: "QRM"
};

$.dataRepository = {
    _cache: $("@namfox.neoseeker.com/data-repository;1").service("nfxIDataRepository"),
    
    getData: function(key) {
        /// <summary>Gets arbitrary data from the data repository based on the key.</summary>
        /// <param name="key" type="String">The key of the data to retrieve. The DataKeys variable has well-known keys whose values already exist in the repository.</param>
        /// <returns type="Object" />
        
        var data = this._cache.getData(key);
        
        if (data) {
            return data.wrappedJSObject;
        }
        
        return null;
    },
    
    setData: function(key, data) {
        /// <summary>Sets arbitrary data in the data repository based on the key.</summary>
        /// <param name="key" type="String">The key of the data to set. The DataKeys variable has well-known keys whose values already exist in the repository.</param>
        /// <param name="data" type="Object">The data to set</param>
        
        this._cache.setData(key, (data === null || data === undefined) ? null : $.wrapObjectLiteral(data));
    }
};

// Tracing
$.trace = $("@namfox.neoseeker.com/trace;1").service("nfxITrace");
$.assert = $.trace.assert;
$.fail = $.trace.fail;
$.info = $.trace.info;
$.verbose = $.trace.verbose;
$.error = function (e) {
    $.trace.error(e, e.stack);
};
$.warn = function (e) {
    $.trace.warn(e, e.stack);
};

$.traceLevels = {};
$.traceLevels.Off = 0;
$.traceLevels.TraceError = 1;
$.traceLevels.TraceWarning = 2;
$.traceLevels.TraceInfo = 3;
$.traceLevels.TraceVerbose = 4;

// Text Transformers
$.transformers = {};
$.transformers.gamespot = $("@namfox.neoseeker.com/transformers/gamespot;1").service("nfxITextTransformer").transform;
$.transformers.gametrailers = $("@namfox.neoseeker.com/transformers/gametrailers;1").service("nfxITextTransformer").transform;
$.transformers.gametrailersUser = $("@namfox.neoseeker.com/transformers/gametrailers-user;1").service("nfxITextTransformer").transform;
$.transformers.gamevideos = $("@namfox.neoseeker.com/transformers/gamevideos;1").service("nfxITextTransformer").transform;

// Url Encoding
var encoder = $("@namfox.neoseeker.com/encoder;1").service("nfxIEncoder");
$.attrEncode = encoder.attrEncode;
$.urlEncode = encoder.urlEncode;

$.dumpObject = function(o) {
    if (o === null || o === undefined) {
        $.verbose("Object passed to dump object is null or undefined.");
        return;
    }
    
    for (var i in o) {
        if (o.hasOwnProperty(i)) {
            $.verbose(i + " : " + o[i]);
        }
    }
};

$.fxVersion = {
    _comparer: $("@mozilla.org/xpcom/version-comparator;1").service("nsIVersionComparator"),
    _version: $("@mozilla.org/xre/app-info;1").service("nsIXULAppInfo").version,
    
    equals: function(version) {
        return this._comparer.compare(this._version, version) === 0;
    },
    
    lessThanOrEqualTo: function(version) {
        return this._comparer.compare(this._version, version) <= 0;
    },
    
    greaterThanOrEqualTo: function(version) {
        return this._comparer.compare(this._version, version) >= 0;
    }
};

$.wrapObjectLiteral = function(o) {
    /// <summary>Wraps an object literal to send to XPCOM.</summary>
    /// <param name="o" type="Object">The Object to wrap before pushing it to XPCOM.</param>
    /// <returns type="Object" />
    
    return {
        wrappedJSObject: o
    };
};

$.contains = function(str, substring) {
    /// <summary>Returns a value indicating whether the specified String object occurs within this string.</summary>
    /// <param name="str" type="String">The String object in which to search for the substring.</param>
    /// <param name="substring" type="String">The String object to seek.</param>
    /// <returns type="Boolean" />

    return str.indexOf(substring) !== -1;
};

$.toRegExp = function(str, flags) {
    /// <summary>Converts a string into a regular expression (RegExp) object.</summary>
    /// <param name="str" type="String">The String object to convert to a RegExp.</param>
    /// <param name="flags" optional="true">(Optional) Flags to pass to the RegExp constructor.</param>
    /// <returns type="RegExp" />

    return new RegExp(str.replace(/([\\\^\$*+[\]?{}.=!:(|)])/g, "\\$1"), flags || "");
};

$.repeat = function(original, count) {
    /// <summary>
    ///   Creates a string by repeating the given string
    ///   a certain number of times.
    /// </summary>
    /// <param name="original" type="String">
    ///   The original string to repeat a certain number of times.
    /// </param>
    /// <param name="count" type="Number" integer="true">
    ///   The number of times the original string will occur in the result string.
    /// </param>
    /// <returns type="String" />

    var retVal = [];
    for (var i = 0; i < count; ++i) {
        retVal.push(original);
    }
    return retVal.join("");
};

$.startsWith = function(str, substring) {
    /// <summary>Determines whether the beginning of this instance matches the specified string.</summary>
    /// <param name="str" type="String">The String object in which to search for the substring.</param>
    /// <param name="substring" type="String">The String object to seek.</param>
    /// <returns type="Boolean" />

    return str.indexOf(substring) === 0;
};

$.endsWith = function(str, substring) {
    /// <summary>Determines whether the end of this instance matches the specified string.</summary>
    /// <param name="str" type="String">The String object in which to search for the substring.</param>
    /// <param name="substring" type="String">The String object to seek.</param>
    /// <returns type="Boolean" />

    return str.indexOf(substring) === str.length - substring.length;
};

$.trim = function(str) {
    /// <summary>Removes all occurrences of white space characters from the beginning and end of this instance.</summary>
    /// <param name="str" type="String">The String object from which to trim whitespace.</param>
    /// <returns type="String" />

    return str.replace(/^\s+|\s+$/g, "");
};

$.padLeft = function(str, totalWidth, paddingChar) {
    /// <summary>
    /// Right-aligns the characters in this instance, padding on the left
    /// with a specified character for a specified total length.
    /// </summary>
    /// <param name="str" type="String">The String object to pad from the left.</param>
    /// <param name="totalWidth" type="Number">
    /// The number of characters in the resulting string, equal to the
    /// number of original characters plus any additional padding characters.
    /// </param>
    /// <param name="paddingChar" type="String">
    /// A padding character.
    /// </param>
    /// <returns type="String" />

    paddingChar = paddingChar || ' ';
    if (paddingChar.length !== 1) {
        paddingChar = paddingChar[0];
    }

    var padLength = totalWidth - str.length;
    if (padLength > 0) {
        return $.repeat(paddingChar, padLength) + str;
    }

    return str;
};

var Base64 = {
 
    // private property
    _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
 
    // public method for encoding
    encode : function (input) {
        var output = "";
        var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
        var i = 0;
 
        input = Base64._utf8_encode(input);
 
        while (i < input.length) {
 
            chr1 = input.charCodeAt(i++);
            chr2 = input.charCodeAt(i++);
            chr3 = input.charCodeAt(i++);
 
            enc1 = chr1 >> 2;
            enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
            enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
            enc4 = chr3 & 63;
 
            if (isNaN(chr2)) {
                enc3 = enc4 = 64;
            } else if (isNaN(chr3)) {
                enc4 = 64;
            }
 
            output = output +
            this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
            this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
 
        }
 
        return output;
    },
 
    // public method for decoding
    decode : function (input) {
        var output = "";
        var chr1, chr2, chr3;
        var enc1, enc2, enc3, enc4;
        var i = 0;
 
        input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
 
        while (i < input.length) {
 
            enc1 = this._keyStr.indexOf(input.charAt(i++));
            enc2 = this._keyStr.indexOf(input.charAt(i++));
            enc3 = this._keyStr.indexOf(input.charAt(i++));
            enc4 = this._keyStr.indexOf(input.charAt(i++));
 
            chr1 = (enc1 << 2) | (enc2 >> 4);
            chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
            chr3 = ((enc3 & 3) << 6) | enc4;
 
            output = output + String.fromCharCode(chr1);
 
            if (enc3 != 64) {
                output = output + String.fromCharCode(chr2);
            }
            if (enc4 != 64) {
                output = output + String.fromCharCode(chr3);
            }
 
        }
 
        output = Base64._utf8_decode(output);
 
        return output;
 
    },
 
    // private method for UTF-8 encoding
    _utf8_encode : function (string) {
        string = string.replace(/\r\n/g,"\n");
        var utftext = "";
 
        for (var n = 0; n < string.length; n++) {
 
            var c = string.charCodeAt(n);
 
            if (c < 128) {
                utftext += String.fromCharCode(c);
            }
            else if((c > 127) && (c < 2048)) {
                utftext += String.fromCharCode((c >> 6) | 192);
                utftext += String.fromCharCode((c & 63) | 128);
            }
            else {
                utftext += String.fromCharCode((c >> 12) | 224);
                utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                utftext += String.fromCharCode((c & 63) | 128);
            }
 
        }
 
        return utftext;
    },
 
    // private method for UTF-8 decoding
    _utf8_decode : function (utftext) {
        var string = "";
        var i = 0;
        var c = c1 = c2 = 0;
 
        while ( i < utftext.length ) {
 
            c = utftext.charCodeAt(i);
 
            if (c < 128) {
                string += String.fromCharCode(c);
                i++;
            }
            else if((c > 191) && (c < 224)) {
                c2 = utftext.charCodeAt(i+1);
                string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
                i += 2;
            }
            else {
                c2 = utftext.charCodeAt(i+1);
                c3 = utftext.charCodeAt(i+2);
                string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
                i += 3;
            }
 
        }
 
        return string;
    }
 
}

function ManualResetEvent() {
}

ManualResetEvent.prototype = {
    _isSet: false,
    
    reset: function() {
        this._isSet = false;
    },
    
    set: function() {
        this._isSet = true;
    },
    
    wait: function(timeout) {
        /// <summary>Waits for this ManualResetEvent to be set.</summary>
        /// <param name="timeout" type="Number" integer="true" optional="true">
        ///   (Optional) The number of milliseconds to wait before continuing execution.
        /// </param>
        
        var timedOut = false;
        var timedOutSafe = false;
        
        if (timeout) {
            setTimeout(
                function() {
                    timedOut = true;
                },
                timeout
            );
        }
        
        var thread = $("@mozilla.org/thread-manager;1").service("nsIThreadManager").currentThread;
            
        while (!this._isSet) {
            if (timedOut) {
                timedOutSafe = true;
                break;
            }
                
            thread.processNextEvent(true);
        }
        
        // Use timedOutSafe instead of timedOut because there is a race condition here in that
        // timedOut could be set to true between the call to ManualResetEvent.set() (thus exiting the
        // while loop) and this point.
        return !timedOutSafe;
    },
};

Selenium.prototype.doSelectRange = function(locator, specification) {
    var element = this.browserbot.findElement(locator);
    if (element.value === undefined) {
        Assert.fail("Element " + locator + " is not an input.");
    }
    
    var selection = eval("(" + specification + ")");
    
    if (selection.start === -1) {
        selection.start = element.value.length;
    }
    
    if (selection.end === -1) {
        selection.end = element.value.length;
    }
    
    element.focus();
    element.setSelectionRange(selection.start, selection.end);
};

Selenium.prototype.getInnerHtml = function(locator) {
    return this.browserbot.findElement(locator).innerHTML;
};

Selenium.prototype.getTabLocation = function(index) {
    var chromeWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"].
        getService(Components.interfaces.nsIWindowMediator).
        getMostRecentWindow("navigator:browser");
    
    return chromeWindow.gBrowser.getBrowserAtIndex(index).contentDocument.location.href;
};

Selenium.prototype.changeQuickMoveThreadId = function() {
    var chromeWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"].
        getService(Components.interfaces.nsIWindowMediator).
        getMostRecentWindow("navigator:browser");
    
    // Don't really move to evidence!
    chromeWindow.NAMFox.QuickMove.targetForumId = 39757;
};

Selenium.prototype.doCloseTab = function(index) {
    index = parseInt(index);
    var chromeWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"].
        getService(Components.interfaces.nsIWindowMediator).
        getMostRecentWindow("navigator:browser");
    var gBrowser = chromeWindow.gBrowser;
    
    return gBrowser.removeTab(gBrowser.tabContainer.childNodes[index]);
};

Selenium.prototype.doSelectTab = function(index) {
    index = parseInt(index);
    var chromeWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"].
        getService(Components.interfaces.nsIWindowMediator).
        getMostRecentWindow("navigator:browser");
    var gBrowser = chromeWindow.gBrowser;
    
    return gBrowser.selectedTab = gBrowser.tabContainer.childNodes[index];
};

Selenium.prototype.getTabCount = function(index) {
    var chromeWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"].
        getService(Components.interfaces.nsIWindowMediator).
        getMostRecentWindow("navigator:browser");
    var gBrowser = chromeWindow.gBrowser;
    
    return gBrowser.tabContainer.childNodes.length;
};

//////////////////////////////////////////////
// NAMFox Specific Extensions
//////////////////////////////////////////////

Selenium.prototype.doInitialize = function(userName, password) {
    if (!$.getConfig().forumRoot) {
        throw new Error("Could not find the configuration item for the forum and member root URLs. Check that include-tests is true in build.properties.");
    }

    var chromeWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"].
        getService(Components.interfaces.nsIWindowMediator).
        getMostRecentWindow("navigator:browser");
        
    var gBrowser = chromeWindow.gBrowser;
    var browser = gBrowser.getBrowserForTab(gBrowser.tabContainer.childNodes[0]);
    
    var handler = function(event) {
        var htmlDocument = event.originalTarget;
    
        var w = htmlDocument.defaultView;
        if (w.last_refresh_count_ts) {
            // The last_refresh_count_ts variable is used by Neoseeker
            // in order to constantly refresh the user panel (which
            // contain PMs, responses, and other data). Unfortunately
            // these constant HTTP POSTs can interfere with log out
            // operations in the tests, so we effectively disable
            // the constant refresh since it's not important for our tests.
            delete w.last_refresh_count_ts;
        }
    };
    
    browser.addEventListener("load", handler, true);
    this.changeQuickMoveThreadId();

    // Add the user name and password for dev.
    var observerService = $("@mozilla.org/observer-service;1").service("nsIObserverService");
    observerService.addObserver(RequestObserver, "http-on-modify-request", false);

    var passwordManager = Components.classes["@mozilla.org/passwordmanager;1"];
    
    globalUserName = userName;
    globalPassword = password;

    if (passwordManager) {
        passwordManager = passwordManager.getService(Components.interfaces.nsIPasswordManager);
        passwordManager.addUser('http://beta.neoseeker.com', userName, password);
    }
    else {
        passwordManager = $("@mozilla.org/login-manager;1").service("nsILoginManager");
        var loginInfo = $("@mozilla.org/login-manager/loginInfo;1").instance("nsILoginInfo");
        loginInfo.init("http://beta.neoseeker.com", null, "beta.neoseeker.com", userName, password, "", "");
        passwordManager.addLogin(loginInfo);
    }

    // Add the user name and password for dev.
    var observerService = $("@mozilla.org/observer-service;1").service("nsIObserverService");
    observerService.addObserver(RequestObserver, "http-on-modify-request", false);
};

var RequestObserver = {
    observe: function(subject, topic, data) {
        if (topic == "http-on-modify-request") {
            var httpChannel = subject.QueryInterface($("nsIHttpChannel"));

            if (httpChannel.URI.host === "beta.neoseeker.com") {
                var authorization = Base64.encode(globalUserName + ":" + globalPassword);
                httpChannel.setRequestHeader("Authorization", "Basic " + authorization, false);
            }
        }
    }
};

Selenium.prototype.doLogOut = function() {
    var nsIRequest = $("nsIRequest");
    var bypassCache =
        nsIRequest.INHIBIT_PERSISTENT_CACHING |
        nsIRequest.LOAD_BYPASS_CACHE | 
        nsIRequest.LOAD_BACKGROUND;
    var xhr = null;
    
    // Logout is really flaky. Sometimes the tests still see us as logged
    // in even though we aren't. To counteract this, we are making two
    // requests, one to log out and one to confirm the log out. If the
    // log out isn't confirmed, we repeat the process.
    do {
        xhr = $("@mozilla.org/xmlextras/xmlhttprequest;1").instance("nsIXMLHttpRequest");
        xhr.open("GET", $.getConfig().memberRoot + "logout.html", false);
        xhr.channel.loadFlags = bypassCache;
        
        xhr.send(null);
    
        xhr = $("@mozilla.org/xmlextras/xmlhttprequest;1").instance("nsIXMLHttpRequest");
        var threadId = Math.ceil(Math.random() * 1500000);
        xhr.open("GET", $.getConfig().forumRoot + "t" + threadId + "-", false);
        xhr.channel.loadFlags = bypassCache;
        
        try {
            xhr.send(null);
        }
        catch (e) {
            continue;
        }
    } while (xhr.responseText.indexOf("Login</a></div>") === -1);
};

var globalUserName = "";
var globalPassword = "";

Selenium.prototype.doLogin = function(userName, password) {
    var split = userName.split("|", 2);
    
    var autoLogin = split[0] === "True";
    userName = split[1];
    var postData = "login_form=true&username=" + userName + "&password=" + password + "&submitbtn=Login+now";
    globalUserName = userName;
    globalPassword = password;
    
    if (autoLogin) {
        postData += "&save_permauth=true";
    }
    
    var nsIRequest = $("nsIRequest");
    var requestFlags =
        nsIRequest.INHIBIT_PERSISTENT_CACHING |
        nsIRequest.LOAD_BYPASS_CACHE | 
        nsIRequest.LOAD_BACKGROUND;
    
    var authorization = Base64.encode(userName + ":" + password);
    var xhr = $("@mozilla.org/xmlextras/xmlhttprequest;1").instance("nsIXMLHttpRequest");
    xhr.open("GET", $.getConfig().memberRoot + "login.html", false);
    xhr.setRequestHeader("Authorization", "Basic " + authorization);
    xhr.channel.loadFlags = requestFlags;
    xhr.send(null);
    var cookieHeader = xhr.getResponseHeader("Cookie");

    xhr.open("POST", $.getConfig().memberRoot + "login.html", false);
    xhr.channel.loadFlags = requestFlags;
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=ISO-8859-1");
    xhr.setRequestHeader("Cookie", cookieHeader);

    var authorization = Base64.encode(userName + ":" + password);
    xhr.setRequestHeader("Authorization", "Basic " + authorization);
    xhr.send(postData);
};

Selenium.prototype.doClearSessionCookies = function() {
    var cookieManager = $("@mozilla.org/cookiemanager;1").service("nsICookieManager");
    var cookies = cookieManager.enumerator;

    while (cookies.hasMoreElements()) {
        var cookie = cookies.getNext();
        
        cookie = cookie.QueryInterface($("nsICookie"));
        
        // If this is a Neoseeker cookie with no expiration (aka a session cookie)
        if (cookie.host.search(/neoseeker/) !== -1 && cookie.expires === 0) {
            cookieManager.remove(cookie.host, cookie.name, cookie.path, false);
        }
    }
};

Selenium.prototype.doClearLoginInformation = function() {
    $.getCurrentUser().userName = null;
    
    var cookieManager = $("@mozilla.org/cookiemanager;1").service("nsICookieManager");
    
    cookieManager.remove(".neoseeker.com", "neologin", "/", false);
    cookieManager.remove(".neoseeker.com", "ns", "/", false);
};

Selenium.prototype.getCurrentUser = function() {
    var user = $.getCurrentUser();
    
    return [ user.userName, user.isModerator.toString() ];
};

Selenium.prototype.getNAMFoxVersion = function() {
    var extensionManager = Components.classes["@mozilla.org/extensions/manager;1"];

    if (!extensionManager) {
        var waitHandle = new ManualResetEvent();
        var version = "1.0";
        Components.utils.import("resource://gre/modules/AddonManager.jsm");
        AddonManager.getAddonByID("superfastoz@neoseeker.com", function(addon) {
            version = addon.version;
            waitHandle.set();
        });

        waitHandle.wait(10000);

        return version;
    }

    return extensionManager.
        getService(Components.interfaces.nsIExtensionManager).
        getItemForID("superfastoz@neoseeker.com").
        version;
};

//////////////////////////////////////////////
// Messages
//////////////////////////////////////////////

Selenium.prototype.doSetDefaultMessageText = function(text) {
    $.messages.getDefaultMessage().text = text;
};

Selenium.prototype.doSetMessageName = function(key, name) {
    $.messages.getMessage(key).name = name;
};

Selenium.prototype.doSetMessageText = function(key, text) {
    $.messages.getMessage(key).text = text;
};

//////////////////////////////////////////////
// Configuration
//////////////////////////////////////////////

Selenium.prototype.getDisableDoubleClickOnPostsWithMedia = function() {
    return $.getConfig().quickEdit.disableDoubleClickForPostsWithMedia;
};

Selenium.prototype.doSetDisableDoubleClickOnPostsWithMedia = function(value) {
    $.getConfig().quickEdit.disableDoubleClickForPostsWithMedia = parseInt(value);
};

Selenium.prototype.getColorLightness = function() {
    return $.getConfig().stripMarkup.colorLightness;
};

Selenium.prototype.doSetColorLightness = function(value) {
    $.getConfig().stripMarkup.colorLightness = parseInt(value);
};

Selenium.prototype.getStripOptions = function() {
    return $.getConfig().stripMarkup.stripOptions;
};

Selenium.prototype.doSetStripOptions = function(value) {
    $.getConfig().stripMarkup.stripOptions = parseInt(value);
};

Selenium.prototype.getReadReceipt = function() {
    return $.getConfig().privateMessages.readReceipt;
};

Selenium.prototype.doSetReadReceipt = function(value) {
    $.getConfig().privateMessages.readReceipt = value === "True";
};

Selenium.prototype.getRememberQuickReplyMessage = function() {
    return $.getConfig().quickReply.rememberMessage;
};

Selenium.prototype.doSetRememberQuickReplyMessage = function(value) {
    $.getConfig().quickReply.rememberMessage = value === "True";
};

Selenium.prototype.getLinkBehavior = function() {
    return $.getConfig().toolbar.linkBehavior;
};

Selenium.prototype.doSetLinkBehavior = function(value) {
    $.getConfig().toolbar.linkBehavior = parseInt(value);
};

Selenium.prototype.getRegisteredVersion = function() {
    return $.getConfig().registeredVersion;
};

Selenium.prototype.doSetRegisteredVersion = function(value) {
    $.getConfig().registeredVersion = value;
};

Selenium.prototype.getTraceLevel = function() {
    return $.getConfig().diagnostics.traceLevel;
};

Selenium.prototype.doSetTraceLevel = function(value) {
    $.getConfig().diagnostics.traceLevel = value;
};

//////////////////////////////////////////////
// Toolbar
//////////////////////////////////////////////

Selenium.prototype.doClickToolbarMenuItem = function(chromeId, button) {
    var chromeWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"].
        getService(Components.interfaces.nsIWindowMediator).
        getMostRecentWindow("navigator:browser");
        
    var menuItem = chromeWindow.document.getElementById(chromeId);
    var menu = menuItem.parentNode;
    while (menu.nodeName !== "toolbarbutton") {
        menu = menu.parentNode;
    }
    
    // Refreshing the menu.
    menu.open = true;
    menu.open = false;
    
    var mouseButton = 0;
    
    // Note that the mouse button values are different in other browsers.
    switch (button) {
        case "Middle":
            mouseButton = 1;
            break;
        case "Right":
            mouseButton = 2;
            break;
    }
    
    return this.browserbot.clickElement(menuItem, 0, 0, mouseButton);
};


Selenium.prototype.doExecuteToolbarMenuItemCommand = function(chromeId) {
    var chromeWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"].
        getService(Components.interfaces.nsIWindowMediator).
        getMostRecentWindow("navigator:browser");
        
    var menuItem = chromeWindow.document.getElementById(chromeId);
    var menu = menuItem.parentNode;
    while (menu.nodeName !== "toolbarbutton") {
        menu = menu.parentNode;
    }
    
    // Refreshing the menu.
    menu.open = true;
    menu.open = false;
    
    return menuItem.doCommand();
};

Selenium.prototype.getToolbarMenuItemCommand = function(chromeId) {
    var chromeWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"].
        getService(Components.interfaces.nsIWindowMediator).
        getMostRecentWindow("navigator:browser");
        
    var menuItem = chromeWindow.document.getElementById(chromeId);
    var menu = menuItem.parentNode;
    while (menu.nodeName !== "toolbarbutton") {
        menu = menu.parentNode;
    }
    
    // Refreshing the menu.
    menu.open = true;
    menu.open = false;
    
    return menuItem.getAttribute("oncommand");
};

Selenium.prototype.getToolbarMenuItemVisibility = function(chromeId) {
    var chromeWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"].
        getService(Components.interfaces.nsIWindowMediator).
        getMostRecentWindow("navigator:browser");
    
    var menuItem = chromeWindow.document.getElementById(chromeId);
    var menu = menuItem.parentNode;
    while (menu.nodeName !== "toolbarbutton") {
        menu = menu.parentNode;
    }

    // Refreshing the menu.
    menu.open = true;
    menu.open = false;
    
    return chromeWindow.getComputedStyle(menuItem, "").display !== "none";
};

Selenium.prototype.doAddSearchNeoseekerButton = function() {
    var chromeWindow = Components.classes["@mozilla.org/appshell/window-mediator;1"].
        getService(Components.interfaces.nsIWindowMediator).
        getMostRecentWindow("navigator:browser");

    var toolbar = chromeWindow.document.getElementById("namfox-toolbar");

    toolbar.insertItem("namfox-search-neoseeker", null, null, false);
};

//////////////////////////////////////////////
// Ad-hoc verification
//////////////////////////////////////////////

Selenium.prototype.doVerifyTwoRequestsToMemberApis = function() {
    var consoleService = $("@mozilla.org/consoleservice;1").service("nsIConsoleService");
    
    var messages = {};
    var messageCount = {};
    consoleService.getMessageArray(messages, messageCount);

    var count = 0;
    for (var i = 0; i < messageCount.value; ++i) {
        var msg = messages.value[i].message;

        if (msg.indexOf("Making GET request") !== -1 && (msg.indexOf("is_moderator.json") !== -1 || msg.indexOf("get_custom_smilies.json") !== -1)) {
            ++count;
        }
    }

    if (count !== 2) {
        throw new Error("Made " + count + " request(s) to a member API. Expected 2.");
    }
};